/* Copyright (c) 2010, Carl Burch. License information is located in the
 * com.cburch.logisim.Main source code and at www.cburch.com/logisim/. */

package com.cburch.logisim.circuit;

import java.util.Collection;
import java.util.ConcurrentModificationException;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.CopyOnWriteArraySet;

import com.cburch.logisim.circuit.Propagator.SetData;
import com.cburch.logisim.comp.Component;
import com.cburch.logisim.comp.ComponentDrawContext;
import com.cburch.logisim.comp.ComponentState;
import com.cburch.logisim.data.BitWidth;
import com.cburch.logisim.data.Location;
import com.cburch.logisim.data.Value;
import com.cburch.logisim.instance.Instance;
import com.cburch.logisim.instance.InstanceData;
import com.cburch.logisim.instance.InstanceFactory;
import com.cburch.logisim.instance.InstanceState;
import com.cburch.logisim.proj.Project;
import com.cburch.logisim.std.wiring.Clock;
import com.cburch.logisim.std.wiring.Pin;

public class CircuitState implements InstanceData {
    private class MyCircuitListener implements CircuitListener {
        @Override
        public void circuitChanged(CircuitEvent event) {
            int action = event.getAction();
            if (action == CircuitEvent.ACTION_ADD) {
                Component comp = (Component) event.getData();
                if (comp instanceof Wire) {
                    Wire w = (Wire) comp;
                    markPointAsDirty(w.getEnd0());
                    markPointAsDirty(w.getEnd1());
                } else {
                    markComponentAsDirty(comp);
                }
            } else if (action == CircuitEvent.ACTION_REMOVE) {
                Component comp = (Component) event.getData();
                if (comp.getFactory() instanceof SubcircuitFactory) {
                    // disconnect from tree
                    CircuitState substate = (CircuitState) getData(comp);
                    if (substate != null && substate.parentComp == comp) {
                        substates.remove(substate);
                        substate.parentState = null;
                        substate.parentComp = null;
                    }
                }

                if (comp instanceof Wire) {
                    Wire w = (Wire) comp;
                    markPointAsDirty(w.getEnd0());
                    markPointAsDirty(w.getEnd1());
                } else {
                    if (base != null) {
                        base.checkComponentEnds(CircuitState.this, comp);
                    }

                    dirtyComponents.remove(comp);
                }
            } else if (action == CircuitEvent.ACTION_CLEAR) {
                substates.clear();
                wireData = null;
                componentData.clear();
                values.clear();
                dirtyComponents.clear();
                dirtyPoints.clear();
                causes.clear();
            } else if (action == CircuitEvent.ACTION_CHANGE) {
                Object data = event.getData();
                if (data instanceof Collection) {
                    @SuppressWarnings("unchecked")
                    Collection<Component> comps = (Collection<Component>) data;
                    markComponentsDirty(comps);
                    if (base != null) {
                        for (Component comp : comps) {
                            base.checkComponentEnds(CircuitState.this, comp);
                        }
                    }
                } else {
                    Component comp = (Component) event.getData();
                    markComponentAsDirty(comp);
                    if (base != null) {
                        base.checkComponentEnds(CircuitState.this, comp);
                    }

                }
            } else if (action == CircuitEvent.ACTION_INVALIDATE) {
                Component comp = (Component) event.getData();
                markComponentAsDirty(comp);
                // TODO detemine if this should really be missing if (base != null) base.checkComponentEnds(CircuitState.this, comp);
            } else if (action == CircuitEvent.TRANSACTION_DONE) {
                ReplacementMap map = event.getResult().getReplacementMap(circuit);
                if (map != null) {
                    for (Component comp : map.getReplacedComponents()) {
                        Object compState = componentData.remove(comp);
                        if (compState != null) {
                            Class<?> compFactory = comp.getFactory().getClass();
                            boolean found = false;
                            for (Component repl : map.get(comp)) {
                                if (repl.getFactory().getClass() == compFactory) {
                                    found = true;
                                    setData(repl, compState);
                                    break;
                                }
                            }
                            if (!found && compState instanceof CircuitState) {
                                CircuitState sub = (CircuitState) compState;
                                sub.parentState = null;
                                substates.remove(sub);
                            }
                        }
                    }
                }
            }
        }
    }

    private MyCircuitListener myCircuitListener = new MyCircuitListener();
    // base of tree of CircuitStates
    private Propagator base = null;
    // project where circuit lies
    private Project proj;
    // circuit being simulated
    private Circuit circuit;

    // parent in tree of CircuitStates
    private CircuitState parentState = null;
    // subcircuit component containing this state
    private Component parentComp = null;
    private HashSet<CircuitState> substates = new HashSet<CircuitState>();

    private CircuitWires.State wireData = null;
    private HashMap<Component,Object> componentData = new HashMap<Component,Object>();
    private Map<Location,Value> values = new HashMap<Location,Value>();
    private CopyOnWriteArraySet<Component> dirtyComponents = new CopyOnWriteArraySet<Component>();
    private CopyOnWriteArraySet<Location> dirtyPoints = new CopyOnWriteArraySet<Location>();
    HashMap<Location,SetData> causes = new HashMap<Location,SetData>();

    private static int lastId = 0;
    private int id = lastId++;

    public CircuitState(Project proj, Circuit circuit) {
        this.proj = proj;
        this.circuit = circuit;
        circuit.addCircuitListener(myCircuitListener);
    }

    public Project getProject() {
        return proj;
    }

    Component getSubcircuit() {
        return parentComp;
    }

    @Override
    public CircuitState clone() {
        return cloneState();
    }

    public CircuitState cloneState() {
        CircuitState ret = new CircuitState(proj, circuit);
        ret.copyFrom(this, new Propagator(ret));
        ret.parentComp = null;
        ret.parentState = null;
        return ret;
    }

    private void copyFrom(CircuitState src, Propagator base) {
        this.base = base;
        this.parentComp = src.parentComp;
        this.parentState = src.parentState;
        HashMap<CircuitState,CircuitState> substateData = new HashMap<CircuitState,CircuitState>();
        this.substates = new HashSet<CircuitState>();
        for (CircuitState oldSub : src.substates) {
            CircuitState newSub = new CircuitState(src.proj, oldSub.circuit);
            newSub.copyFrom(oldSub, base);
            newSub.parentState = this;
            this.substates.add(newSub);
            substateData.put(oldSub, newSub);
        }
        for (Component key : src.componentData.keySet()) {
            Object oldValue = src.componentData.get(key);
            if (oldValue instanceof CircuitState) {
                Object newValue = substateData.get(oldValue);
                if (newValue != null) {
                    this.componentData.put(key, newValue);
                }

                else {
                    this.componentData.remove(key);
                }

            } else {
                Object newValue;
                if (oldValue instanceof ComponentState) {
                    newValue = ((ComponentState) oldValue).clone();
                } else {
                    newValue = oldValue;
                }
                this.componentData.put(key, newValue);
            }
        }
        for (Location key : src.causes.keySet()) {
            Propagator.SetData oldValue = src.causes.get(key);
            Propagator.SetData newValue = oldValue.cloneFor(this);
            this.causes.put(key, newValue);
        }
        if (src.wireData != null) {
            this.wireData = (CircuitWires.State) src.wireData.clone();
        }
        this.values.putAll(src.values);
        this.dirtyComponents.addAll(src.dirtyComponents);
        this.dirtyPoints.addAll(src.dirtyPoints);
    }

    @Override
    public String toString() {
        return "State" + id + "[" + circuit.getName() + "]";
    }

    //
    // public methods
    //
    public Circuit getCircuit() {
        return circuit;
    }

    public CircuitState getParentState() {
        return parentState;
    }

    // returns Set of CircuitStates
    public Set<CircuitState> getSubstates() {
        return substates;
    }

    public Propagator getPropagator() {
        if (base == null) {
            base = new Propagator(this);
            markAllComponentsDirty();
        }
        return base;
    }

    public void drawOscillatingPoints(ComponentDrawContext context) {
        if (base != null) {
            base.drawOscillatingPoints(context);
        }

    }

    public Object getData(Component comp) {
        return componentData.get(comp);
    }

    public void setData(Component comp, Object data) {
        if (data instanceof CircuitState) {
            CircuitState oldState = (CircuitState) componentData.get(comp);
            CircuitState newState = (CircuitState) data;
            if (oldState != newState) {
                // There's something new going on with this subcircuit.
                // Maybe the subcircuit is new, or perhaps it's being
                // removed.
                if (oldState != null && oldState.parentComp == comp) {
                    // it looks like it's being removed
                    substates.remove(oldState);
                    oldState.parentState = null;
                    oldState.parentComp = null;
                }
                if (newState != null && newState.parentState != this) {
                    // this is the first time I've heard about this CircuitState
                    substates.add(newState);
                    newState.base = this.base;
                    newState.parentState = this;
                    newState.parentComp = comp;
                    newState.markAllComponentsDirty();
                }
            }
        }
        componentData.put(comp, data);
    }

    public Value getValue(Location pt) {
        Value ret = values.get(pt);
        if (ret != null) {
            return ret;
        }


        BitWidth wid = circuit.getWidth(pt);
        return Value.createUnknown(wid);
    }

    public void setValue(Location pt, Value val, Component cause, int delay) {
        if (base != null) {
            base.setValue(this, pt, val, cause, delay);
        }

    }

    public void markComponentAsDirty(Component comp) {
        try {
            dirtyComponents.add(comp);
        } catch (RuntimeException e) {
            CopyOnWriteArraySet<Component> set = new CopyOnWriteArraySet<Component>();
            set.add(comp);
            dirtyComponents = set;
        }
    }

    public void markComponentsDirty(Collection<Component> comps) {
        dirtyComponents.addAll(comps);
    }

    public void markPointAsDirty(Location pt) {
        dirtyPoints.add(pt);
    }

    public InstanceState getInstanceState(Component comp) {
        Object factory = comp.getFactory();
        if (factory instanceof InstanceFactory) {
            return ((InstanceFactory) factory).createInstanceState(this, comp);
        } else {
            throw new RuntimeException("getInstanceState requires instance component");
        }
    }

    public InstanceState getInstanceState(Instance instance) {
        Object factory = instance.getFactory();
        if (factory instanceof InstanceFactory) {
            return ((InstanceFactory) factory).createInstanceState(this, instance);
        } else {
            throw new RuntimeException("getInstanceState requires instance component");
        }
    }

    //
    // methods for other classes within package
    //
    public boolean isSubstate() {
        return parentState != null;
    }

    void processDirtyComponents() {
        if (!dirtyComponents.isEmpty()) {
            // This seeming wasted copy is to avoid ConcurrentModifications
            // if we used an iterator instead.
            Object[] toProcess;
            RuntimeException firstException = null;
            for (int tries = 4; true; tries--) {
                try {
                    toProcess = dirtyComponents.toArray();
                    break;
                } catch (RuntimeException e) {
                    if (firstException == null) {
                        firstException = e;
                    }

                    if (tries == 0) {
                        toProcess = new Object[0];
                        dirtyComponents = new CopyOnWriteArraySet<Component>();
                        throw firstException;
                    }
                }
            }
            dirtyComponents.clear();
            for (Object compObj : toProcess) {
                if (compObj instanceof Component) {
                    Component comp = (Component) compObj;
                    comp.propagate(this);
                    if (comp.getFactory() instanceof Pin && parentState != null) {
                        // should be propagated in superstate
                        parentComp.propagate(parentState);
                    }
                }
            }
        }

        CircuitState[] subs = new CircuitState[substates.size()];
        for (CircuitState substate : substates.toArray(subs)) {
            substate.processDirtyComponents();
        }
    }

    void processDirtyPoints() {
        HashSet<Location> dirty = new HashSet<Location>(dirtyPoints);
        dirtyPoints.clear();
        if (circuit.wires.isMapVoided()) {
            for (int i = 3; i >= 0; i--) {
                try {
                    dirty.addAll(circuit.wires.points.getSplitLocations());
                    break;
                } catch (ConcurrentModificationException e) {
                    // try again...
                    try { Thread.sleep(1); } catch (InterruptedException e2) { }
                    if (i == 0) {
                        e.printStackTrace();
                    }

                }
            }
        }
        if (!dirty.isEmpty()) {
            circuit.wires.propagate(this, dirty);
        }

        CircuitState[] subs = new CircuitState[substates.size()];
        for (CircuitState substate : substates.toArray(subs)) {
            substate.processDirtyPoints();
        }
    }

    void reset() {
        wireData = null;
        for (Iterator<Component> it = componentData.keySet().iterator(); it.hasNext(); ) {
            Component comp = it.next();
            if (!(comp.getFactory() instanceof SubcircuitFactory)) {
                it.remove();
            }

        }
        values.clear();
        dirtyComponents.clear();
        dirtyPoints.clear();
        causes.clear();
        markAllComponentsDirty();

        for (CircuitState sub : substates) {
            sub.reset();
        }
    }

    boolean tick(int ticks) {
        boolean ret = false;
        for (Component clock : circuit.getClocks()) {
            ret |= Clock.tick(this, ticks, clock);
        }

        CircuitState[] subs = new CircuitState[substates.size()];
        for (CircuitState substate : substates.toArray(subs)) {
            ret |= substate.tick(ticks);
        }
        return ret;
    }

    CircuitWires.State getWireData() {
        return wireData;
    }

    void setWireData(CircuitWires.State data) {
        wireData = data;
    }

    Value getComponentOutputAt(Location p) {
        // for CircuitWires - to get values, ignoring wires' contributions
        Propagator.SetData cause_list = causes.get(p);
        return Propagator.computeValue(cause_list);
    }

    Value getValueByWire(Location p) {
        return values.get(p);
    }

    void setValueByWire(Location p, Value v) {
        // for CircuitWires - to set value at point
        boolean changed;
        if (v == Value.NIL) {
            Object old = values.remove(p);
            changed = (old != null && old != Value.NIL);
        } else {
            Object old = values.put(p, v);
            changed = !v.equals(old);
        }
        if (changed) {
            boolean found = false;
            for (Component comp : circuit.getComponents(p)) {
                if (!(comp instanceof Wire) && !(comp instanceof Splitter)) {
                    found = true;
                    markComponentAsDirty(comp);
                }
            }
            // NOTE: this will cause a double-propagation on components
            // whose outputs have just changed.

            if (found && base != null) {
                base.locationTouched(this, p);
            }

        }
    }

    //
    // private methods
    //
    private void markAllComponentsDirty() {
        dirtyComponents.addAll(circuit.getNonWires());
    }
}
